55. 绘制直方图

⭐ 本章导入

Listing 1
import pandas as pd  # 导入Pandas数据分析库
import numpy as np  # 导入NumPy数值计算库
import matplotlib.pyplot as plt  # 导入Matplotlib绑图库
plt.rcParams['font.sans-serif'] = ['Source Han Serif SC', 'SimHei', 'Microsoft YaHei']  # 设置中文字体
plt.rcParams['axes.unicode_minus'] = False  # 解决负号显示问题

⭐ 直方图是什么?

直方图(Histogram)是展示数据分布最直观的工具。

在金融分析中,直方图帮助我们:

  • 理解分布形态:数据是对称、偏斜还是多峰
  • 识别异常值:远离主分布的孤立值
  • 评估风险:分布的尾部形态决定极端事件概率
  • 比较群体:不同资产、不同时期的分布差异

⭐ 直方图的数学原理——分箱与计数

直方图是概率密度函数(PDF)的离散近似,分为三步:

  1. 分箱(Binning):将数据范围分成 \(k\) 个区间(bin)
  2. 计数(Counting):统计每个区间内的数据点数
  3. 归一化(Normalization):转换为概率或密度

⭐ 直方图的数学定义

设数据集 \(X = \{x_1, x_2, \ldots, x_n\}\),分箱边界为 \(b_0, b_1, \ldots, b_k\)

\[\text{count}_i = |\{x_j | b_{i-1} \leq x_j < b_i\}|\]

频率(Frequency):

\[f_i = \frac{\text{count}_i}{n}\]

密度(Density):

\[d_i = \frac{f_i}{b_i - b_{i-1}} = \frac{\text{count}_i}{n \cdot (b_i - b_{i-1})}\]

⭐ 平台任务1解答代码

以下代码与教学平台任务要求完全一致:

Listing 2
# 注:平台任务代码,依赖平台指定的外部数据URL
# ⚠️ 平台原始代码 - 请原样输入至教学平台(注释除外),平台才会判定答案正确
import pandas as pd  # 导入Pandas数据分析库
import numpy as np  # 导入NumPy数值计算库

# 从Excel文件读取数据存入stock_price
stock_price = pd.read_excel("https://huoran.oss-cn-shenzhen.aliyuncs.com/20220824/xlsx/1562273067622227968.xlsx",sheet_name="Sheet1",header=0,index_col=0)

stock_price = stock_price.dropna()   #删除缺失值所在的行

stock_return = np.log(stock_price/stock_price.shift(1))  #计算股票收益率

stock_return = stock_return.dropna()     #删除缺失值所在的行

stock_return.describe()  # 查看stock_return的描述性统计量

stock_return = stock_return.dropna()         #删除缺失值所在的行

print(stock_return)  # 输出股票数据

⭐ 平台任务2解答代码

Listing 3
# 注:平台任务代码,依赖平台指定的外部数据URL
# ⚠️ 平台原始代码 - 请原样输入至教学平台(注释除外),平台才会判定答案正确
import pandas as pd  # 导入Pandas数据分析库
import numpy as np  # 导入NumPy数值计算库
import matplotlib.pyplot as plt  # 导入Matplotlib绑图库
plt.rcParams["font.sans-serif"] = ["SimHei"]  # 设置Matplotlib全局参数
plt.rcParams['axes.unicode_minus']=False  # 设置Matplotlib全局参数

# 从Excel文件读取数据存入stock_price
stock_price = pd.read_excel("https://huoran.oss-cn-shenzhen.aliyuncs.com/20220824/xlsx/1562273067622227968.xlsx",sheet_name="Sheet1",header=0,index_col=0)
stock_price = stock_price.dropna()   #删除缺失值所在的行

stock_return = np.log(stock_price/stock_price.shift(1))  #计算股票收益率
stock_return = stock_return.dropna()     #删除缺失值所在的行
plt.figure(figsize=(9,9))  # 创建图形画布
plt.subplot(2,1,1)  # 选择子图位置
plt.plot(stock_return["东方航空(A股)"],"r-",label=u"东方航空(A股)",lw=2)  # 绑制折线图
plt.xticks(fontsize=13)  # 设置X轴刻度标签
plt.yticks(fontsize=13)  # 设置Y轴刻度标签
plt.ylim(-0.12,0.15)  # 设置Y轴显示范围
plt.ylabel(u"收益率",fontsize=13,rotation=90)  # 设置Y轴标签
plt.legend(loc=9,fontsize=13)     #图例放在中上位置
plt.grid()  #加入网格线
plt.subplot(2,1,2) #代表第二行的子图
plt.plot(stock_return["东方航空(美股)"],"b-",label=u"东方航空(美股)",lw=2)  # 绑制折线图
plt.xticks(fontsize=13)  # 设置X轴刻度标签
plt.xlabel(u"日期",fontsize=13)  # 设置X轴标签
plt.yticks(fontsize=13)  # 设置Y轴刻度标签
plt.ylim(-0.12,0.15)  # 设置Y轴显示范围
plt.ylabel(u"收益率",fontsize=13,rotation=90)  # 设置Y轴标签
plt.legend(loc=9,fontsize=13)     #图例放在中上位置
plt.grid()  #加入网格线
plt.show()  # 显示图形
plt.savefig("1.png")  # 保存图形至文件

⭐ 平台任务3解答代码

以下代码与教学平台任务要求完全一致:

Listing 4
# 注:平台任务代码,依赖平台指定的外部数据URL及tkinter模块
# ⚠️ 平台原始代码 - 请原样输入至教学平台(注释除外),平台才会判定答案正确
from tkinter import font  # 导入font模块
import pandas as pd  # 导入Pandas数据分析库
import numpy as np  # 导入NumPy数值计算库
import matplotlib.pyplot as plt  # 导入Matplotlib绑图库
plt.rcParams["font.sans-serif"] = ["SimHei"]  # 设置Matplotlib全局参数
plt.rcParams['axes.unicode_minus']=False  # 设置Matplotlib全局参数

# 从Excel文件读取数据存入stock_price
stock_price = pd.read_excel("https://huoran.oss-cn-shenzhen.aliyuncs.com/20220824/xlsx/1562273067622227968.xlsx",sheet_name="Sheet1",header=0,index_col=0)
stock_price = stock_price.dropna()   #删除缺失值所在的行

stock_return = np.log(stock_price/stock_price.shift(1))  #计算股票收益率
stock_return = stock_return.dropna()     #删除缺失值所在的行
SHI_return = np.array(stock_return.iloc[:,2:]) #将上海石化A股和美股日收益率转为数组形式
plt.figure(figsize=(9,10))  # 创建图形画布
plt.subplot(2,1,1)  # 选择子图位置
plt.hist(SHI_return,label=[u"上海石化A股日收益率",u"上海石化美股日收益率"],stacked=True,edgecolor="k",bins=30)  #以堆叠形式展出
plt.xticks(fontsize=13)  # 设置X轴刻度标签
plt.yticks(fontsize=13)  # 设置Y轴刻度标签
plt.ylabel(u"频数",fontsize=13,rotation=90)  # 设置Y轴标签
plt.legend(fontsize=13)  # 添加图例
plt.grid()  # 显示网格线
plt.subplot(2,1,2)  # 选择子图位置
plt.hist(SHI_return,label=[u"上海石化A股日收益率",u"上海石化美股日收益率"],edgecolor="k",bins=30) #以并排形式展出
plt.xticks(fontsize=13)  # 设置X轴刻度标签
plt.yticks(fontsize=13)  # 设置Y轴刻度标签
plt.ylabel(u"频数",fontsize=13,rotation=90)  # 设置Y轴标签
plt.xlabel(u"股票日收益率",fontsize=13)  # 设置X轴标签
plt.legend(fontsize=13)  # 添加图例
plt.grid()  # 显示网格线
plt.savefig("2.png")  # 保存图形至文件

⭐ 平台任务4解答代码

以下代码与教学平台任务要求完全一致:

Listing 5
# 注:平台任务代码,依赖平台指定的外部数据URL
# ⚠️ 平台原始代码 - 请原样输入至教学平台(注释除外),平台才会判定答案正确
import pandas as pd  # 导入Pandas数据分析库
import numpy as np  # 导入NumPy数值计算库
import matplotlib.pyplot as plt  # 导入Matplotlib绑图库
plt.rcParams["font.sans-serif"] = ["SimHei"]  # 设置Matplotlib全局参数
plt.rcParams['axes.unicode_minus']=False  # 设置Matplotlib全局参数

# 从Excel文件读取数据存入stock_price
stock_price = pd.read_excel("https://huoran.oss-cn-shenzhen.aliyuncs.com/20220824/xlsx/1562273067622227968.xlsx",sheet_name="Sheet1",header=0,index_col=0)
stock_price = stock_price.dropna()   #删除缺失值所在的行

stock_return = np.log(stock_price/stock_price.shift(1))  #计算股票收益率
stock_return = stock_return.dropna()     #删除缺失值所在的行
CEA_return = np.array(stock_return.iloc[:,0:2]) #将东方航空A股和美股收益率转为数组形式
plt.figure(figsize=(9,10))  # 创建图形画布
plt.subplot(2,1,1)  # 选择子图位置
plt.hist(CEA_return,label=[u"东方航空A股日收益率",u"东方航空美股日收益率"],stacked=True,edgecolor="k",bins=30)  #以堆叠形式展出
plt.xticks(fontsize=13)  # 设置X轴刻度标签
plt.yticks(fontsize=13)  # 设置Y轴刻度标签
plt.ylabel(u"频数",fontsize=13,rotation=90)  # 设置Y轴标签
plt.legend(fontsize=13)  # 添加图例
plt.grid()  # 显示网格线
plt.subplot(2,1,2)  # 选择子图位置
plt.hist(CEA_return,label=[u"东方航空A股日收益率",u"东方航空美股日收益率"],edgecolor="k",bins=30) #以并排形式展出
plt.xticks(fontsize=13)  # 设置X轴刻度标签
plt.yticks(fontsize=13)  # 设置Y轴刻度标签
plt.ylabel(u"频数",fontsize=13,rotation=90)  # 设置Y轴标签
plt.xlabel(u"股票日收益率",fontsize=13)  # 设置X轴标签
plt.legend(fontsize=13)  # 添加图例
plt.grid()  # 显示网格线
plt.savefig("3.png")  # 保存图形至文件

⭐ 关键参数解析

绘制直方图时需关注以下参数:

  • bins(箱数):太少信息损失,太多噪声干扰
  • edgecolor:箱边颜色,'white' 使分箱更清晰
  • alpha:透明度,0.7 使图表不显沉重

bins 的经验法则

  • \(k = \sqrt{n}\)(平方根法则)
  • \(k = \log_2(n)\)(Sturges 法则)

⭐ 概率密度与直方图叠加

# 生成正态分布数据
mu, sigma = 0.001, 0.02  # mu:均值,sigma:标准差
data = np.random.normal(mu, sigma, 1000)

# 绘制直方图(密度)
plt.figure(figsize=(10, 6))
# density=True:将Y轴归一化为概率密度(总面积为1)
n, bins, patches = plt.hist(data, bins=30, density=True,
                            color='#2E86AB', alpha=0.7,
                            edgecolor='white', label='样本分布')

# 叠加理论正态分布曲线
x = np.linspace(data.min(), data.max(), 100)
pdf = (1 / (sigma * np.sqrt(2 * np.pi))) * np.exp(-0.5 * ((x - mu) / sigma)**2)
plt.plot(x, pdf, color='#E3120B', linewidth=2.5, label='理论正态分布')

plt.title('收益率分布与正态拟合', fontsize=16, fontweight='bold')
plt.xlabel('收益率', fontsize=12)
plt.ylabel('概率密度', fontsize=12)
plt.legend(fontsize=11)
plt.grid(axis='y', alpha=0.3)
plt.tight_layout()
plt.show()
Listing 6: 概率密度曲线与直方图叠加

⭐ 正态分布公式与拟合检验

正态分布的数学形式

\[ f(x) = \frac{1}{\sigma\sqrt{2\pi}} \exp\left(-\frac{(x-\mu)^2}{2\sigma^2}\right) \]

其中:\(\mu\) 为均值(位置参数),\(\sigma\) 为标准差(尺度参数)

拟合检验——KS 检验

  • 比较经验分布与理论分布
  • p 值 > 0.05:无法拒绝正态假设
  • p 值 ≤ 0.05:拒绝正态假设

⭐ KS 检验代码演示

Listing 7
from scipy import stats

mu, sigma = 0.001, 0.02
data = np.random.normal(mu, sigma, 1000)

# KS 检验:比较经验分布与理论正态分布
ks_stat, ks_pvalue = stats.kstest(data, 'norm', args=(mu, sigma))
print(f'Kolmogorov-Smirnov检验:')
print(f'统计量: {ks_stat:.4f}')
print(f'p值: {ks_pvalue:.4f}')
print(f'结论: {"无法拒绝正态假设" if ks_pvalue > 0.05 else "拒绝正态假设"}')
Kolmogorov-Smirnov检验:
统计量: 0.0443
p值: 0.0382
结论: 拒绝正态假设

⭐ 分组直方图——不同股票收益率对比

# 生成三只股票的收益率数据
stocks = {
    '贵州茅台': np.random.normal(0.001, 0.018, 500),   # 低波动
    '中国平安': np.random.normal(0.0008, 0.025, 500),   # 中波动
    '中小板指': np.random.normal(0.0015, 0.035, 500)    # 高波动
}

fig, axes = plt.subplots(1, 3, figsize=(15, 5))
colors = ['#E3120B', '#008080', '#2C3E50']

for ax, (stock, data), color in zip(axes, stocks.items(), colors):
    ax.hist(data, bins=25, color=color, alpha=0.7, edgecolor='white')
    ax.axvline(data.mean(), color='black', linestyle='--', linewidth=2)
    ax.set_title(f'{stock}\nμ={data.mean():.4f}, σ={data.std():.4f}',
                 fontsize=12, fontweight='bold')
    ax.set_xlabel('收益率', fontsize=10)
    ax.set_ylabel('频数', fontsize=10)
    ax.grid(axis='y', alpha=0.3)

plt.tight_layout()
plt.show()
Listing 8: 分组直方图——不同股票收益率对比

⭐ 年化波动率计算

Listing 9
stocks = {
    '贵州茅台': np.random.normal(0.001, 0.018, 500),
    '中国平安': np.random.normal(0.0008, 0.025, 500),
    '中小板指': np.random.normal(0.0015, 0.035, 500)
}

print('各股票波动率比较:')
for stock, data in stocks.items():
    # 年化波动率 = 日波动率 × sqrt(252)
    annual_volatility = data.std() * np.sqrt(252)
    print(f'{stock}: 年化波动率 = {annual_volatility:.2%}')
各股票波动率比较:
贵州茅台: 年化波动率 = 27.64%
中国平安: 年化波动率 = 40.89%
中小板指: 年化波动率 = 55.04%

⭐ 可视化策略对比

绘制分组直方图有三种常见方案:

方案 优点 缺点
子图对比 保持每个分布的完整性 难以直接比较
叠加直方图 直观对比分布差异 颜色叠加可能混淆
核密度估计(KDE) 平滑的分布曲线 细节可能被平滑掉

⭐ 累积直方图——收益率分位数

returns = np.random.normal(0.001, 0.02, 1000)

plt.figure(figsize=(10, 6))
# cumulative=True:绘制累积分布
# density=True:归一化为累积概率
plt.hist(returns, bins=30, cumulative=True, density=True,
         color='#008080', alpha=0.7, edgecolor='white',
         label='累积分布')

# 标注关键分位数
percentiles = [5, 25, 50, 75, 95]
for p in percentiles:
    value = np.percentile(returns, p)
    plt.axvline(value, color='#E3120B', linestyle=':', alpha=0.6)
    plt.text(value, 0.5, f'P{p}', fontsize=9, rotation=90)

plt.title('收益率累积分布', fontsize=16, fontweight='bold')
plt.xlabel('收益率', fontsize=12)
plt.ylabel('累积概率', fontsize=12)
plt.legend(fontsize=11)
plt.grid(axis='y', alpha=0.3)
plt.tight_layout()
plt.show()
Listing 10: 累积直方图——收益率分位数

⭐ 累积分布函数与金融应用

累积分布函数(CDF):

\[ F(x) = P(X \leq x) = \int_{-\infty}^x f(t) dt \]

金融应用

  • VaR 计算:VaR at 95% = \(F^{-1}(0.05)\)
  • 收益排序:快速定位表现最好的 p% 时间段
  • 比较分布:两个 CDF 图可以清晰看出差异

⭐ 分位数分析

Listing 11
returns = np.random.normal(0.001, 0.02, 1000)

print('分位数分析:')
for p in [5, 25, 50, 75, 95]:
    value = np.percentile(returns, p)
    print(f'P{p}: {value:.4f}')
分位数分析:
P5: -0.0318
P25: -0.0121
P50: 0.0010
P75: 0.0143
P95: 0.0343
  • P5:有 5% 的概率低于此值(95% VaR)
  • P50:中位数
  • P95:有 95% 的概率低于此值

⭐ 二维直方图——两资产联合分布

# 生成相关的二维数据
mean = [0.001, 0.0008]
cov = [[0.0004, 0.0002], [0.0002, 0.0006]]  # 协方差矩阵
data_2d = np.random.multivariate_normal(mean, cov, 1000)

x = data_2d[:, 0]  # 股票A收益率
y = data_2d[:, 1]  # 股票B收益率

plt.figure(figsize=(10, 8))
plt.hist2d(x, y, bins=30, cmap='Blues')
plt.colorbar(label='频数')

# 添加均值线
plt.axvline(x.mean(), color='red', linestyle='--', linewidth=2, alpha=0.7)
plt.axhline(y.mean(), color='red', linestyle='--', linewidth=2, alpha=0.7)

plt.title('两资产收益率联合分布', fontsize=16, fontweight='bold')
plt.xlabel('股票A收益率', fontsize=12)
plt.ylabel('股票B收益率', fontsize=12)
plt.grid(alpha=0.3)
plt.tight_layout()
plt.show()

correlation = np.corrcoef(x, y)[0, 1]
print(f'相关系数: {correlation:.4f}')
Listing 12: 二维直方图——两资产收益率联合分布
相关系数: 0.3559

⭐ 二维分布的数学含义

联合概率密度\(f_{X,Y}(x, y)\)

相关系数

\[ \rho_{XY} = \frac{\text{Cov}(X, Y)}{\sigma_X \sigma_Y} \]

金融应用

  • 分散化效果:相关系数越接近 0,分散化效果越好
  • 对冲策略:负相关的资产可以相互对冲
  • 因子暴露:多资产对共同因子的敏感度

⭐ 核密度估计(KDE)——平滑分布曲线

from scipy.stats import gaussian_kde

# 生成对数正态分布数据(右偏分布)
data_skewed = np.random.lognormal(0, 0.5, 1000)

fig, axes = plt.subplots(1, 2, figsize=(14, 6))

# 左:直方图
axes[0].hist(data_skewed, bins=30, density=True,
             color='#2E86AB', alpha=0.7, edgecolor='white')
axes[0].set_title('直方图(离散)', fontsize=14, fontweight='bold')
axes[0].set_xlabel('数值', fontsize=12)
axes[0].set_ylabel('密度', fontsize=12)
axes[0].grid(axis='y', alpha=0.3)

# 右:KDE
kde = gaussian_kde(data_skewed)
x_range = np.linspace(data_skewed.min(), data_skewed.max(), 500)
axes[1].plot(x_range, kde(x_range), color='#E3120B', linewidth=2.5)
axes[1].fill_between(x_range, kde(x_range), alpha=0.3, color='#E3120B')
axes[1].set_title('核密度估计(连续)', fontsize=14, fontweight='bold')
axes[1].set_xlabel('数值', fontsize=12)
axes[1].set_ylabel('密度', fontsize=12)
axes[1].grid(axis='y', alpha=0.3)

plt.tight_layout()
plt.show()

skewness_val = pd.Series(data_skewed).skew()
print(f'偏度: {skewness_val:.4f}')
print(f'结论: {"右偏分布" if skewness_val > 0 else "左偏分布" if skewness_val < 0 else "对称分布"}')
Listing 13: 核密度估计——平滑分布曲线
偏度: 1.6083
结论: 右偏分布

⭐ KDE 的数学原理

核密度估计公式

\[ \hat{f}_h(x) = \frac{1}{nh}\sum_{i=1}^n K\left(\frac{x-x_i}{h}\right) \]

其中:

  • \(K(\cdot)\):核函数(常用高斯核)
  • \(h\):带宽(bandwidth),控制平滑程度

带宽选择

  • \(h\) 太小 → 过度拟合,噪声干扰
  • \(h\) 太大 → 过度平滑,细节丢失
  • Silverman 法则\(h = 1.06\sigma n^{-1/5}\)

⭐ 金融收益率的正态性检验

from scipy import stats

np.random.seed(42)
n = 1000

# 生成具有厚尾特征的金融收益率
normal_sample = np.random.normal(0, 0.02, n)
financial_returns = normal_sample + np.random.standard_t(3, n) * 0.01

fig, axes = plt.subplots(1, 2, figsize=(14, 6))

# 左:直方图 vs 正态分布
axes[0].hist(financial_returns, bins=40, density=True,
             color='#2E86AB', alpha=0.7, edgecolor='white', label='实际分布')
x = np.linspace(financial_returns.min(), financial_returns.max(), 100)
theoretical_norm = stats.norm.pdf(x, 0, 0.02)
axes[0].plot(x, theoretical_norm, 'r-', linewidth=2, label='理论正态')
axes[0].set_title('实际分布 vs 正态分布', fontsize=14, fontweight='bold')
axes[0].set_xlabel('收益率', fontsize=12)
axes[0].set_ylabel('密度', fontsize=12)
axes[0].legend()
axes[0].grid(axis='y', alpha=0.3)

# 右:Q-Q图
stats.probplot(financial_returns, dist='norm', plot=axes[1])
axes[1].set_title('Q-Q图(正态性检验)', fontsize=14, fontweight='bold')
axes[1].grid(alpha=0.3)

plt.tight_layout()
plt.show()
Listing 14: 金融收益率的正态性检验

⭐ 正态性检验结果

Listing 15
from scipy import stats

np.random.seed(42)
n = 1000
normal_sample = np.random.normal(0, 0.02, n)
financial_returns = normal_sample + np.random.standard_t(3, n) * 0.01

print(f'偏度: {pd.Series(financial_returns).skew():.4f}')
print(f'峰度: {pd.Series(financial_returns).kurtosis():.4f}')

# Shapiro-Wilk 检验
shapiro_stat, shapiro_p = stats.shapiro(financial_returns[:5000])
print(f'\nShapiro-Wilk检验: 统计量={shapiro_stat:.4f}, p值={shapiro_p:.4f}')

# Jarque-Bera 检验
jb_stat, jb_p = stats.jarque_bera(financial_returns)
print(f'Jarque-Bera检验: 统计量={jb_stat:.4f}, p值={jb_p:.4f}')
print(f'结论: {"拒绝正态假设" if jb_p < 0.05 else "无法拒绝正态假设"}')
偏度: -0.0386
峰度: 0.6775

Shapiro-Wilk检验: 统计量=0.9957, p值=0.0063
Jarque-Bera检验: 统计量=18.8494, p值=0.0001
结论: 拒绝正态假设

⭐ Q-Q 图解读指南

如果数据服从正态分布,Q-Q 图上散点应落在红色直线上。

偏离模式

  • 尾部向上翘起 → 厚尾分布(金融数据典型特征)
  • 左端向下 → 左偏(更多负的极端值)
  • 右端向上 → 右偏(更多正的极端值)

金融意义

  • 厚尾:极端事件概率高于正态预测
  • VaR 低估:正态假设会低估在险价值
  • 黑天鹅:需要考虑非正态分布的风险模型

⭐ 本章小结

直方图类型 核心用途 关键参数
基础直方图 查看单变量分布 bins, edgecolor
密度直方图 与理论分布对比 density=True
分组直方图 对比多个分布 子图 / 叠加
累积直方图 分位数分析 cumulative=True
二维直方图 联合分布 plt.hist2d()
核密度估计 平滑分布曲线 gaussian_kde